// a plain repo page needs none of the git machinery, so it all lives behind // dynamic imports: the object store loads on first use, each view in its own chunk import type { GitRepo, TransferProgress } from "./git/repo.ts"; import type { Commit } from "./git/types.ts"; import { PAGE_SIZE, paginate } from "./pagination.tsx"; import { canonical, pageAt, type Site, viewAt } from "./route.ts"; import { humanSize, identityDate } from "./util.ts"; const segments = location.pathname.split("/").filter(Boolean); const reserved = new Set(["css", "js", "-"]); interface LoadingProgress { bar: HTMLProgressElement; amount: Text; transfers: Map; show: () => void; } function initRouter(repoName: string, base: string) { const main = document.querySelector("main"); if (!main) return; const ref = main.dataset.ref ?? null; const tip = main.dataset.tip ?? null; const loadedPathname = location.pathname; let loading: LoadingProgress | null = null; const owners = new Map(); const showProgress = (transfer: TransferProgress) => { if (transfer.phase === "start" && loading) { owners.set(transfer.id, loading); loading.show(); } const owner = owners.get(transfer.id); if (!owner) return; owner.transfers.set(transfer.id, { loaded: transfer.loaded, total: transfer.total }); if (transfer.phase === "done") owners.delete(transfer.id); if (owner !== loading) return; let loaded = 0; let total = 0; let unknown = false; for (const item of owner.transfers.values()) { loaded += item.loaded; if (item.total === undefined) unknown = true; else total += item.total; } if (unknown) owner.bar.removeAttribute("value"); else { owner.bar.max = Math.max(total, 1); owner.bar.value = loaded; } owner.amount.data = unknown ? ` ${humanSize(loaded)}` : ` ${humanSize(loaded)} / ${humanSize(total)}`; }; let transfers = new AbortController(); let repo: Promise | undefined; const gitRepo = () => repo ??= import("./git/repo.ts").then(({ GitRepo, httpFetcher }) => new GitRepo(httpFetcher(base, showProgress, () => transfers.signal), `sorcery${base}`) ); // the static page's own hooks; its path exists at the tip by construction const page = pageAt(loadedPathname, ref, tip); if (tip) { for (const heading of main.querySelectorAll("h2.log-heading")) { const target = canonical(page, { kind: "history", oid: tip, path: [] }, "tree"); heading.replaceChildren({heading.textContent}); } for (const actions of main.querySelectorAll(".topbar > .actions")) { const target = canonical(page, { kind: "history", oid: tip, path: page.path }, page.kind); actions.prepend(history); } for (const item of main.querySelectorAll(".languages li[data-language]")) { const label = item.querySelector("span")!; const target = canonical(page, { kind: "language", oid: tip, path: [item.dataset.language!] }, null); label.replaceWith({[...label.childNodes]}); } } for (const span of main.querySelectorAll("[data-commit]")) { const oid = span.dataset.commit!; span.replaceWith({span.textContent}); } // Plain-mode blob pages ship unhighlighted source; data-hl carries the path. for (const pre of main.querySelectorAll("pre.src[data-hl]")) { const targets = pre.querySelectorAll(".code-text"); const source = [...targets].map(line => line.textContent ?? "").join("\n"); void import("./highlight.ts") .then(module => module.highlightedLines(pre.dataset.hl!, source)) .then(highlighted => { if (highlighted === null) return; for (const [i, line] of highlighted.entries()) targets[i]?.replaceChildren(...line); }) .catch(err => console.warn("arborium:", err)); } initLogPagination(gitRepo, main); // the repo header stays; everything else makes way for the view const original = ([...main.children] as HTMLElement[]).filter(el => !el.matches("header.repo")); let view: HTMLElement | null = null; let current = ""; const route = async () => { if (location.href === current) return; current = location.href; // the previous view's in-flight fetches would only steal bandwidth now transfers.abort(); transfers = new AbortController(); const target = location.href; const parsed = viewAt(pageAt(location.pathname, ref, tip), location.hash); loading = null; if (!parsed) { // only the loaded page's static content is here to show if (location.pathname !== loadedPathname) return location.reload(); view?.remove(); view = null; for (const el of original) el.style.display = ""; return; } const close = ← close; const status = (

loading {parsed.kind} {parsed.oid.slice(0, 12)}…

) as HTMLElement; const nextView = (
{close} {status}
) as HTMLElement; let shown = false; const show = () => { if (shown || location.href !== target) return; shown = true; for (const el of original) el.style.display = "none"; view?.remove(); view = nextView; main.append(view); }; const bar = document.createElement("progress"); const amount = document.createTextNode(""); status.append(" ", bar, amount); loading = { bar, amount, transfers: new Map(), show }; try { const site: Site = { repo: await gitRepo(), page: pageAt(location.pathname, ref, tip), name: repoName }; const rendered = parsed.kind === "commit" ? await (await import("./commit.tsx")).commitView(site, parsed) : parsed.kind === "tree" ? await (await historicalView()).treeView(site, parsed) : parsed.kind === "blob" ? await (await historicalView()).blobView(site, parsed) : parsed.kind === "language" ? await (await historicalView()).languageView(site, parsed) : await (await historicalView()).historyView(site, parsed); if (location.href === target) { loading = null; nextView.replaceChildren(close, ...rendered); show(); } } catch (err) { if (location.href === target) { loading = null; nextView.replaceChildren(close,

failed to load {parsed.kind}: {String(err)}

); show(); } } }; // canonical links may change the pathname: take those in-app rather than // loading the fallback page, unless the link *is* the fallback page document.addEventListener("click", event => { const anchor = (event.target as Element).closest("a[href]"); if ( !(anchor instanceof HTMLAnchorElement) || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || anchor.target ) return; const url = new URL(anchor.href); if (url.origin !== location.origin || url.pathname === location.pathname || !url.hash) return; if (!url.pathname.startsWith(`${base}/`)) return; event.preventDefault(); history.pushState(null, "", url); void route(); }); addEventListener("popstate", route); addEventListener("hashchange", route); void route(); } async function* commitLog(gitRepo: () => Promise, frontier: string[]): AsyncGenerator { const [repo, { log }] = await Promise.all([gitRepo(), import("./git/walk.ts")]); yield* log(repo, frontier, PAGE_SIZE); } function initLogPagination(gitRepo: () => Promise, main: HTMLElement) { for (const control of main.querySelectorAll("[data-log-frontier]")) { const sibling = control.previousElementSibling; if (!(sibling instanceof HTMLOListElement)) continue; const items = [...sibling.children] as HTMLElement[]; const frontier = control.dataset.logFrontier!.split(" "); paginate({ list: sibling, control, seed: { items, keys: items.map(li => li.dataset.oid!) }, open: () => commitLog(gitRepo, frontier), key: commit => commit.oid, render: commit => { const href = `#commit/${commit.oid}`; const item = (
  • ) as HTMLLIElement; if (commit.changeId) { item.append({commit.changeId.slice(0, 8)}, " "); } item.append( {commit.oid.slice(0, 7)}, " ", {commit.author.name} , {commit.message.split("\n")[0]}, ); return item; }, }); } } // dynamic imports are memoized by the module loader, so no caching needed const historicalView = () => import("./historical.tsx"); if (segments.length >= 2 && !reserved.has(segments[0])) { initRouter(segments[1], `/${segments[0]}/${segments[1]}`); }